package com.shelpe.services.lexicon.service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.shelpe.services.lexicon.constants.Device;
import com.shelpe.services.lexicon.entity.AdgroupRecord;
import com.shelpe.services.lexicon.entity.KeywordMetric;
import com.shelpe.services.lexicon.entity.KeywordRecommend;
import com.shelpe.services.lexicon.entity.Scope;
import com.shelpe.services.lexicon.repository.mongo.AdgroupRecordRepository;
import com.shelpe.services.lexicon.repository.mongo.KeywordMetricRepository;
import com.shelpe.services.lexicon.simba.InsightWordData;
import com.shelpe.services.lexicon.simba.WordClient;
import com.shelpe.services.lexicon.simba.WordClient.CategoryWordResponse;
import com.shelpe.services.lexicon.simba.WordClient.GlobalWordRespone;
import com.shelpe.services.lexicon.simba.WordClient.NetworkWordResponse;
import com.shelpe.services.lexicon.utils.MetricUtils;

import lombok.extern.slf4j.Slf4j;
import rx.Observable;
import rx.functions.Action1;

@Service
@Slf4j
public class KeywordMetricService{

	private final static String[] TEMPLATE = new String[0];

	@Autowired
	private WordClient wordClient;
	@Autowired
	private KeywordMetricRepository keywordMetricRepository;
	@Autowired
	private AdgroupRecordRepository adgroupRecordRepository;
	@Value("${simbaproxy.insight.count.perQuery:100}")
	private int countPerQuery;
	@Value("${simbaproxy.insight.maxQuery:3}")
	private int maxCount;
	
	public void syncGlobalInsightData4Recommends(final List<KeywordRecommend> recommends, final Date start, final Date end, 
			final AdgroupRecord record, final Callback completeCallback) {
		final WordChunk chunk = new WordChunk(this.countPerQuery);
		for(KeywordRecommend recommend: recommends){
			chunk.addWord(recommend.getWord());
		}
		log.info("start to sync global insight data: {}", record);
		final WordChunk.ChunkCallback callback = new WordChunk.ChunkCallback(){
			@Override
			public void call(WordChunk chunk, List<KeywordRecommend> krs, WordChunk.ChunkCallback next) {
				if(chunk == null){
					log.info("finish to sync global insight data: {}", record);
					completeCallback.call(recommends);
					return;
				}
				syncGlobalPerChunk(recommends, start, end, record, chunk, next);
			}
		};
		this.syncGlobalPerChunk(recommends, start, end, record, chunk, callback);
	}
	
	public void syncInsightData4Recommends(final List<KeywordRecommend> recommends, final Date start, final Date end, 
			final AdgroupRecord record, final Callback completeCallback) {
		final WordChunk chunk = new WordChunk(this.countPerQuery);
		for(KeywordRecommend recommend: recommends){
			chunk.addWord(recommend.getWord());
		}
		log.info("start to sync global & category & network insight data: {}", record);
		final WordChunk.ChunkCallback callback = new WordChunk.ChunkCallback(){
			@Override
			public void call(WordChunk chunk, List<KeywordRecommend> krs, WordChunk.ChunkCallback next) {
				if(chunk == null){
					log.info("finish to sync global & category & network insight data: {}", record);
					completeCallback.call(recommends);
					return;
				}
				syncInsightDataPerChunk(recommends, start, end, record, chunk, next);
			}
		};
		this.syncInsightDataPerChunk(recommends, start, end, record, chunk, callback);
	}
	
	public void syncGlobalAndCategoryInsightData4Recommends(final List<KeywordRecommend> recommends, final Date start, final Date end, 
			final AdgroupRecord record, final Callback completeCallback) {
		final WordChunk chunk = new WordChunk(this.countPerQuery);
		for(KeywordRecommend recommend: recommends){
			chunk.addWord(recommend.getWord());
		}
		
		log.info("start to sync global & category insight data: {}", record);
		final WordChunk.ChunkCallback callback = new WordChunk.ChunkCallback(){
			@Override
			public void call(WordChunk chunk, List<KeywordRecommend> krs, WordChunk.ChunkCallback next) {
				if(chunk == null){
					log.info("finish to sync global & category insight data: {}", record);
					completeCallback.call(recommends);
					return;
				}
				syncGlobalAndCategoryInsightDataPerChunk(recommends, start, end, record, chunk, next);
			}
		};
		this.syncGlobalAndCategoryInsightDataPerChunk(recommends, start, end, record, chunk, callback);
	}
	
	public void syncInsightDataPerChunk(final List<KeywordRecommend> recommends, final Date start, final Date end, 
			final AdgroupRecord record, final WordChunk chunk, final WordChunk.ChunkCallback next) {		
		if(chunk.getWords() == null || chunk.getWords().size() == 0){
			next.call(chunk.getNext(), recommends, next);
			return;
		}
		log.info("start to sync global & category & network insight data per chunk: {}", record);
		final int totalRuns = 3;
		final AtomicInteger indexer = new AtomicInteger();
		this.syncGlobalData(chunk, start, end, record, recommends, new Callback(){
			@Override
			public void call(List<KeywordRecommend> krs) {
				if(indexer.incrementAndGet() == totalRuns){
					next.call(chunk.getNext(), recommends, next);
				}
			}
		});
		this.syncCategoryData(chunk, start, end, record, recommends, new Callback(){
			@Override
			public void call(List<KeywordRecommend> krs) {
				if(indexer.incrementAndGet() == totalRuns){
					next.call(chunk.getNext(), recommends, next);
				}
			}
		});
		syncNetworkData(chunk, start, end, record, recommends, new Callback(){
			@Override
			public void call(List<KeywordRecommend> krs) {
				if(indexer.incrementAndGet() == totalRuns){
					next.call(chunk.getNext(), recommends, next);
				}
			}
		});
	}
	
	public void syncGlobalAndCategoryInsightDataPerChunk(final List<KeywordRecommend> recommends, final Date start, final Date end, 
			final AdgroupRecord record, final WordChunk chunk, final WordChunk.ChunkCallback next) {	
		if(chunk.getWords() == null || chunk.getWords().size() == 0){
			next.call(chunk.getNext(), recommends, next);
			return;
		}
		log.info("start to sync global & category insight data per chunk: {}", record);
		final int totalRuns = 2;
		final AtomicInteger indexer = new AtomicInteger();
		this.syncGlobalData(chunk, start, end, record, recommends, new Callback(){
			@Override
			public void call(List<KeywordRecommend> krs) {
				if(indexer.incrementAndGet() == totalRuns){
					next.call(chunk.getNext(), recommends, next);
				}
			}
		});
		this.syncCategoryData(chunk, start, end, record, recommends, new Callback(){
			@Override
			public void call(List<KeywordRecommend> krs) {
				if(indexer.incrementAndGet() == totalRuns){
					next.call(chunk.getNext(), recommends, next);
				}
			}
		});
	}
	
	public void syncGlobalPerChunk(final List<KeywordRecommend> recommends, final Date start, final Date end, 
			final AdgroupRecord record, final WordChunk chunk, final WordChunk.ChunkCallback next) {	
		if(chunk.getWords() == null || chunk.getWords().size() == 0){
			next.call(chunk.getNext(), recommends, next);
			return;
		}
		log.info("start to sync global insight data per chunk: {}", record);
		final int totalRuns = 1;
		final AtomicInteger indexer = new AtomicInteger();
		this.syncGlobalData(chunk, start, end, record, recommends, new Callback(){
			@Override
			public void call(List<KeywordRecommend> krs) {
				if(indexer.incrementAndGet() == totalRuns){
					next.call(chunk.getNext(), recommends, next);
				}
			}
		});
	}

	public void syncGlobalData(final WordChunk chunk, final Date start, final Date end, 
			final AdgroupRecord record, final List<KeywordRecommend> recommends, final Callback next) {
		if(chunk == null || chunk.getWords() == null || chunk.getWords().size() == 0){
			next.call(new ArrayList<KeywordRecommend>());
			return;
		}
		
		Observable<GlobalWordRespone> globalObserver = this.wordClient.findGlobalData(chunk.getWords().toArray(TEMPLATE),
				DateFormatUtils.format(start, "yyyy-MM-dd"), DateFormatUtils.format(end, "yyyy-MM-dd"),
				record.getTanentID());
		globalObserver.subscribe(new Action1<GlobalWordRespone>(){
			@Override
			public void call(GlobalWordRespone res) {
				if (res != null && res.getRes()!=null && res.getRes().getDataList() != null && res.getRes().getDataList().getWords() != null) {
					log.debug("globalObserver get success {}: {}", record, res);
					List<KeywordMetric> metrics = convert2Metric(res.getRes().getDataList().getWords(), start, end, record);
					
					for(KeywordRecommend recommend: recommends){
						KeywordMetric metric = MetricUtils.find(metrics, recommend);
						if(metric != null){
							recommend.setInsight(metric.getMetric());
						}
					}
				}else{
					log.error("globalObserver throw failure {}: {}", record, res);
				}
				next.call(recommends);
			}
			
		}, new Action1<Throwable>(){
			@Override
			public void call(Throwable t) {
				log.debug("globalObserver throw exception {}: {}", record, t);
				next.call(new ArrayList<KeywordRecommend>());
			}
		});
	}
	
	public void syncCategoryData(final WordChunk chunk, final Date start, final Date end, 
			final AdgroupRecord record, final List<KeywordRecommend> recommends, final Callback next) {
		if(chunk == null || chunk.getWords() == null || chunk.getWords().size() == 0){
			next.call(new ArrayList<KeywordRecommend>());
			return;
		}
		
		Observable<CategoryWordResponse> catObserver = this.wordClient.findCategoryData(chunk.getWords().toArray(TEMPLATE),
				DateFormatUtils.format(start, "yyyy-MM-dd"), DateFormatUtils.format(end, "yyyy-MM-dd"),
				record.getRootCategoryID()+ "", record.getTanentID());
		catObserver.subscribe(new Action1<CategoryWordResponse>(){
			@Override
			public void call(CategoryWordResponse res) {
				if (res != null && res.getRes()!=null && res.getRes().getDataList() != null && res.getRes().getDataList().getWords() != null) {
					log.debug("catObserver get success {}: {}", record, res);
					convert2Metric(res.getRes().getDataList().getWords(), start, end, record);
				}else{
					log.error("catObserver throw failure {}: {}", record, res);
				}
				next.call(recommends);
			}
			
		}, new Action1<Throwable>(){
			@Override
			public void call(Throwable t) {
				log.debug("catObserver throw exception {}: {}", record, t);
				next.call(new ArrayList<KeywordRecommend>());
			}
			
		});
	}
	
	public void syncNetworkData(final WordChunk chunk, final Date start, final Date end, 
			final AdgroupRecord record, final List<KeywordRecommend> recommends, final Callback next) {
		if(chunk == null || chunk.getWords() == null || chunk.getWords().size() == 0){
			next.call(new ArrayList<KeywordRecommend>());
			return;
		}
		
		Observable<NetworkWordResponse> networkObserver = this.wordClient.findNetworkData(chunk.getWords().toArray(TEMPLATE),
				DateFormatUtils.format(start, "yyyy-MM-dd"), DateFormatUtils.format(end, "yyyy-MM-dd"),
				record.getTanentID());
		networkObserver.subscribe(new Action1<NetworkWordResponse>(){
			@Override
			public void call(NetworkWordResponse res) {
				if (res != null && res.getRes()!=null && res.getRes().getDataList() != null && res.getRes().getDataList().getWords() != null) {
					log.debug("networkObserver get success {}: {}", record, res);
					List<KeywordMetric> metrics = convert2Metric(res.getRes().getDataList().getWords(), start, end, record);
					CollectionUtils.filter(metrics, new Predicate(){
						@Override
						public boolean evaluate(Object object) {
							KeywordMetric metric = (KeywordMetric) object;
							return metric.getDevice() == Device.mobile;
						}
					});
					for(KeywordRecommend recommend: recommends){
						KeywordMetric metric = MetricUtils.find(metrics, recommend);
						if(metric != null){
							recommend.setMobileInsight(metric.getMetric());
						}
					}
				}else{
					log.error("networkObserver throw failure {}: {}", record, res);
				}
				next.call(recommends);
			}
			
		}, new Action1<Throwable>(){
			@Override
			public void call(Throwable t) {
				log.debug("networkObserver throw exception {}: {}", record, t);
				next.call(new ArrayList<KeywordRecommend>());
			}
			
		});
	}
	
	private List<KeywordMetric> convert2Metric(List<InsightWordData> words, Date start, Date end, AdgroupRecord record){
		List<KeywordMetric> metrics = new ArrayList<KeywordMetric>();
		Scope scope = new Scope();
		scope.setEndTime(end);
		scope.setStartTime(start);

		for (InsightWordData data : words) {
			KeywordMetric km = new KeywordMetric();
			km.from(data);
			km.setAdgroupRecordID(record.getRecordID());
			km.setAdgroupID(record.getAdgroupID());
			km.setScope(scope);

			km.setCreateTime(new Date());
			km.setLastModifiedTime(new Date());
			this.keywordMetricRepository.save(km);
			metrics.add(km);

			record.addKeywordMetric(km);
		}
		record.setLastModifiedTime(new Date());
		this.adgroupRecordRepository.save(record);
		
		return metrics;
	}
	
	public static interface Callback{
		void call(List<KeywordRecommend> recommends);
	}
}
